package nl.Viewer.Sessions.Communication;

import nl.Viewer.SerializedObjects.Parameter.ParameterType;

/**
 * A utility for converting between Java and Python objects/representations. Support is somewhat limited, as (for example) nesting of Python arrays,
 * tuples, etc. is not supported. 
 * 
 *  NB. This object is available in both the server- and the client code, modifications will very likely be need to be carried out on either side.  
 * 
 * @author Jeroen Verschuur
 *
 */

public class JavaPythonUtil {

   /********************************************************************************************************************
    * Really basic parsing of strings representing Python objects to java objects (no nesting allowed!)
    * 
    */

   private static Object parsePythonArrayObject( String pythonObject, boolean checkTuple ) throws ParameterException {
      String opening = checkTuple ? "(" : "[";
      String closing = checkTuple ? ")" : "]";

      // check for tuple array
      if( pythonObject.startsWith( opening ) && pythonObject.endsWith( closing ) ) {
         pythonObject = pythonObject.substring( 1, pythonObject.length() - 1 );

         // split on ','
         String[] elements = pythonObject.split( "," );
         Object[] elementObjects = new Object[ elements.length ];

         int i = 0;
         for( String s : elements ) {
            elementObjects[i++] = parsePythonBasicObject( s );
         }
         return elementObjects;
      }

      throw new ParameterException( "Could not parse this object as an array or tuple" );
   }

   private static Object parsePythonBasicObject( String pythonObject ) throws ParameterException {
      pythonObject = pythonObject.trim();

      if( pythonObject == null || pythonObject.equalsIgnoreCase( "None" ) )
         return null;

      if( pythonObject.equalsIgnoreCase( "True" ) ) {
         return Boolean.TRUE;
      } else if( pythonObject.equalsIgnoreCase( "False" ) ) {
         return Boolean.FALSE;
      }

      try {
         return Integer.parseInt( pythonObject );
      }
      catch( NumberFormatException nfe ) {
      }

      try {
         return Double.parseDouble( pythonObject );
      }
      catch( NumberFormatException nfe ) {
      }

      if( pythonObject.startsWith( "'" ) && pythonObject.endsWith( "'" ) ) {
         return pythonObject.substring( 1, pythonObject.length() - 1 );
      }

      throw new ParameterException( "Could not parse python object as basic type:" + pythonObject );
   }

   public static Object parsePythonObjectToJavaObject( String pythonObject ) throws ParameterException {
      if( pythonObject == null )
         return null;

      pythonObject = pythonObject.trim();
      if( pythonObject.equalsIgnoreCase( "None" ) )
         return null;

      // try tuple of objects
      try {
         return parsePythonArrayObject( pythonObject, true );
      }
      catch( ParameterException pe ) {
      }

      // try array of objects
      try {
         return parsePythonArrayObject( pythonObject, false );
      }
      catch( ParameterException pe ) {
      }

      // try basic type
      try {
         return parsePythonBasicObject( pythonObject );
      }
      catch( ParameterException pe ) {
      }

      throw new ParameterException( "Could not parse python object " + pythonObject + " to java object" );
   }

   public static String parseJavaObjectToPythonObject( Object input, boolean arrayIsTuple ) throws ParameterException {
      if( input == null )
         return "None";

      if( input instanceof Boolean ) {
         if( ( (Boolean)input ) == Boolean.TRUE ) {
            return "True";
         }
         return "False";
      }

      if( input instanceof Integer || input instanceof Double ) {
         return "" + input;
      }

      if( input instanceof String ) {
         return "'" + input + "'";
      }

      if( input instanceof Object[] ) {
         String output;
         if( arrayIsTuple ) {
            output = "(";
         } else {
            output = "[";
         }
         boolean first = true;
         for( Object o : (Object[])input ) {
            if( !first ) {
               output += ", ";
            } else {
               first = false;
            }
            output += parseJavaObjectToPythonObject( o, arrayIsTuple );
         }
         if( arrayIsTuple ) {
            output += ")";
         } else {
            output += "]";
         }
         return output;
      }

      throw new ParameterException( "Could not parse Java object " + input + " to Python object" );
   }

   public static Object castObject( Object input, ParameterType type ) throws ParameterException {
      if( input == null )
         return null;

      if( type == ParameterType.INT ) {
         if( input instanceof Integer )
            return input;

         if( input instanceof Integer[] && ( (Integer[])input ).length == 1 ) {
            System.err.println( "Casting from Integer[1] to Integer, the client should not send an array type object" );
            return ( (Integer[])input )[0];
         }

         if( input instanceof Double ) {
            System.err.println( "Casting from double to int, precision may be lost" );
            return new Integer( (int)Math.round( (Double)input ) );
         }

         if( input instanceof String ) {
            System.err.println( "Casting from string to int, this may fail or precision may be lost" );
            try {
               return new Integer( Integer.parseInt( (String)input ) );
            }
            catch( NumberFormatException nfe ) {
               // ignore, this will default to error message below
            }
         }

         if( input instanceof Boolean ) {
            System.err.println( "Casting from boolean to int, precision will be lost" );
            if( ( (Boolean)input ) == Boolean.TRUE ) {
               return 1;
            } else if( ( (Boolean)input ) == Boolean.FALSE ) {
               return 0;
            }
         }

         throw new ParameterException( "Could not cast from " + input.getClass() + " to INT" );
      } else if( type == ParameterType.DOUBLE ) {
         if( input instanceof Double )
            return input;

         if( input instanceof Double[] && ( (Double[])input ).length == 1 ) {
            System.err.println( "Casting from Double[1] to Double, the client should not send an array type object" );
            return ( (Double[])input )[0];
         }

         if( input instanceof Integer )
            return new Double( (Integer)input );

         if( input instanceof String ) {
            System.err.println( "Casting from string to double, this may fail and precision may be lost" );
            try {
               return new Double( Double.parseDouble( (String)input ) );
            }
            catch( NumberFormatException nfe ) {
               // ignore, this will default to error message below
            }
         }

         if( input instanceof Boolean ) {
            System.err.println( "Casting from boolean to double, precision will be lost" );
            if( ( (Boolean)input ) == Boolean.TRUE ) {
               return Double.valueOf( 1.0 );
            } else if( ( (Boolean)input ) == Boolean.FALSE ) {
               return Double.valueOf( 0.0 );
            }
         }

         throw new ParameterException( "Could not cast from " + input.getClass() + " to DOUBLE" );
      } else if( type == ParameterType.STRING ) {
         if( input instanceof Integer || input instanceof Double || input instanceof String )
            return input.toString();

         if( input instanceof String[] && ( (String[])input ).length == 1 ) {
            System.err.println( "Casting from String[1] to String, the client should not send an array type object" );
            return ( (String[])input )[0];
         }

         throw new ParameterException( "Could not cast from " + input.getClass() + " to STRING" );
      } else if( type == ParameterType.BOOLEAN ) {
         if( input instanceof Boolean )
            return input;

         if( input instanceof Boolean[] && ( (Boolean[])input ).length == 1 ) {
            System.err.println( "Casting from Boolean[1] to Boolean, the client should not send an array type object" );
            return ( (Boolean[])input )[0];
         }

         if( input instanceof Integer ) {
            Boolean output;

            if( ( (Integer)input ) > 0 ) {
               output = Boolean.TRUE;
            } else {
               output = Boolean.FALSE;
            }
            System.err.println( "Casting from Integer(" + input + ") to Boolean(" + output + ")" );
            return output;
         }

         if( input instanceof Double ) {
            Boolean output;

            if( ( (Double)input ) > 0 ) {
               output = Boolean.TRUE;
            } else {
               output = Boolean.FALSE;
            }
            System.err.println( "Casting from Double(" + input + ") to Boolean(" + output + ")" );
            return output;
         }

         if( input instanceof String ) {
            if( ( (String)input ).equalsIgnoreCase( "true" ) ) {
               return Boolean.TRUE;
            } else if( ( (String)input ).equalsIgnoreCase( "false" ) ) {
               return Boolean.FALSE;
            }
         }
         throw new ParameterException( "Could not cast from " + input.getClass() + " to DOUBLE" );

      }

      throw new ParameterException( "ParameterType unknown: " + type );
   }

   private static int getObjectCount( Object input ) throws ParameterException {
      if( input instanceof Integer || input instanceof Double || input instanceof String || input instanceof Boolean ) {
         return 1;
      } else if( input instanceof Integer[] || input instanceof Double[] || input instanceof String[]
         || input instanceof Boolean[] ) {
         return ( (Object[])input ).length;
      } else if( input instanceof double[] ) {
         return ( (double[])input ).length;
      } else if( input instanceof int[] ) {
         return ( (int[])input ).length;
      } else if( input instanceof boolean[] ) {
         return ( (boolean[])input ).length;
      } else if( input instanceof Object[] ) {
         return ( (Object[])input ).length;
      } else if( input == null )
         return 1;

      throw new ParameterException( "Object " + input + " of type " + input.getClass()
         + " not recognized for getParameterLength" );

   }

   public static Object convertJavaObjectToParameter( Object input, ParameterType type, int parameterLength )
         throws ParameterException {
      // check parameter length

      // Object []parameters = convertToObjectArray(input);
      int parameterCount = getObjectCount( input );
      if( input != null && parameterCount != parameterLength ) {
         throw new ParameterException( "Parameter length mismatch, input supplied " + parameterCount
            + " parameter(s) where parameterLength is " + parameterLength );
      }

      // now try to cast all values to the correct type
      if( parameterCount == 1 ) {
         return castObject( input, type );
      } else {
         Object[] output = new Object[ parameterCount ];
         // double[] and int[] are not of type Object[] (duh!), so casting them with the code in the else-part below
         // will throw an exception
         if( input instanceof double[] ) {
            double[] array = (double[])input;
            for( int i = 0; i < parameterCount; i++ ) {
               output[i] = castObject( array[i], type );
            }
         } else if( input instanceof int[] ) {
            int[] array = (int[])input;

            for( int i = 0; i < parameterCount; i++ ) {
               output[i] = castObject( array[i], type );
            }
         } else if( input instanceof boolean[] ) {
            boolean[] array = (boolean[])input;

            for( int i = 0; i < parameterCount; i++ ) {
               output[i] = castObject( array[i], type );
            }
         } else {
            Object[] array = (Object[])input;

            for( int i = 0; i < parameterCount; i++ ) {
               output[i] = castObject( array[i], type );
            }
         }
         return output;
      }
   }

}
